<script id="draw-shader-vs" type="x-shader/x-vertex">
precision highp float;

attribute vec3 inPos;
attribute vec4 inAttr;

varying vec3  vertPos;
varying vec4  vertTex;
varying float highlight;

uniform mat4 u_projection;
uniform mat4 u_view;
uniform mat4 u_model;

uniform mat4 u_rubiks_model;
uniform int  u_cube_hit;
uniform int  u_side_hit;
uniform int  u_cubeInstanceID;

void main()
{   
    vec4 tex     = inAttr;
    int  cube_i  = u_cubeInstanceID;
    int  color_i = int(tex.z + 0.5); 
    int  c_3     = cube_i / 3;
    int  c_9     = cube_i / 9;
    int  x_i     = cube_i - c_3 * 3;
    int  y_i     = (cube_i - c_9 * 9) / 3;
    int  z_i     = cube_i / 9;

    if ( color_i == 1 )
        tex.z = x_i == 0 ? tex.z : 0.0;
    else if ( color_i == 2 )
        tex.z = x_i == 2 ? tex.z : 0.0;
    else if ( color_i == 3 )
        tex.z = y_i == 0 ? tex.z : 0.0;
    else if ( color_i == 4 )
        tex.z = y_i == 2 ? tex.z : 0.0;
    else if ( color_i == 5 )
        tex.z = z_i == 0 ? tex.z : 0.0;
    else if ( color_i == 6 )
        tex.z = z_i == 2 ? tex.z : 0.0;

    mat4 model_view = u_view * u_model * u_rubiks_model;
    vec4 vertex_pos = model_view * vec4(inPos, 1.0);

    vertPos     = vertex_pos.xyz;
    vertTex     = tex;
    highlight   = tex.z > 0.5 && cube_i == u_cube_hit && color_i == u_side_hit ? 1.0 : 0.0;		

    gl_Position = u_projection * vertex_pos;
}
</script>

<script id="draw-shader-fs" type="x-shader/x-fragment">
precision mediump float;

varying vec3  vertPos;
varying vec4  vertTex;
varying float highlight;

void main()
{
    int color_i = int(vertTex.z + 0.5);

    vec4 color;
    if ( color_i == 1 )
        color = vec4(1.0, 0.0, 0.0, 1.0);
    else if ( color_i == 2 )
        color = vec4(0.0, 1.0, 0.0, 1.0);
    else if ( color_i == 3 )
        color = vec4(0.0, 0.0, 1.0, 1.0);
    else if ( color_i == 4 )
        color = vec4(1.0, 0.5, 0.0, 1.0);
    else if ( color_i == 5 )
        color = vec4(1.0, 1.0, 0.0, 1.0);
    else if ( color_i == 6 )
        color = vec4(1.0, 0.0, 1.0, 1.0);
    else 
        color = vec4(0.5, 0.5, 0.5, 1.0);

    color.rgb *= max(0.5, highlight);

    gl_FragColor  = color;
}
</script>

<style>
html,body { margin: 0; overflow: hidden; }
#gui { position : absolute; top : 0; left : 0; color: #CCF; font-size : large; }
#ref-link { position : absolute; bottom : 0; left : 0; font-size : large; }  
</style>

<body>

<div>
<form id="gui" name="inputs">
  <table>
      <tr> <td> <font color=#40f040><spawn id="line_0"></font> </td> </tr> 
      <tr> <td> <font color=#40f040><spawn id="line_1"></font> </td>  </tr>
  </table>
</form>
</div>

<!--div id="ref-link">
  <a href=""> 
  </a>
</div-->

<canvas id="canvas" style="border: none;"></canvas>

<script type="text/javascript">

(function loadscene() {

let gl;

class App {

    constructor() {
        this.canvas = document.getElementById( "canvas");
        gl = canvas.getContext( "experimental-webgl" );
        //gl = canvas.getContext( "webgl2" );
        if ( !gl )
            return null;

        this.progDraw = new ShProg( 
            [ { source : "draw-shader-vs", stage : gl.VERTEX_SHADER },
                { source : "draw-shader-fs", stage : gl.FRAGMENT_SHADER }
            ] );
        if ( !this.progDraw.progObj )
            return null;
        this.progDraw.inPos  = this.progDraw.AttrI("inPos");
        this.progDraw.inAttr = this.progDraw.AttrI("inAttr");
        
        let cube_attrib = [
            // left
            -1.0,   1.0, -1.0,    0.0, 0.0, 1.0, 0.0, 
            -1.0,  -1.0, -1.0,    0.0, 1.0, 1.0, 0.0,
            -1.0,  -1.0,  1.0,    1.0, 1.0, 1.0, 0.0,
            -1.0,   1.0,  1.0,    1.0, 0.0, 1.0, 0.0,

            // right
             1.0,  -1.0, -1.0,    0.0, 0.0, 2.0, 0.0, 
             1.0,   1.0, -1.0,    0.0, 1.0, 2.0, 0.0,
             1.0,   1.0,  1.0,    1.0, 1.0, 2.0, 0.0,
             1.0,  -1.0,  1.0,    1.0, 0.0, 2.0, 0.0,

            // front
            -1.0,  -1.0, -1.0,    0.0, 0.0, 3.0, 0.0, 
             1.0,  -1.0, -1.0,    0.0, 1.0, 3.0, 0.0,
             1.0,  -1.0,  1.0,    1.0, 1.0, 3.0, 0.0,
            -1.0,  -1.0,  1.0,    1.0, 0.0, 3.0, 0.0,

            // back
             1.0,   1.0, -1.0,    0.0, 0.0, 4.0, 0.0, 
            -1.0,   1.0, -1.0,    0.0, 1.0, 4.0, 0.0,
            -1.0,   1.0,  1.0,    1.0, 1.0, 4.0, 0.0,
             1.0,   1.0,  1.0,    1.0, 0.0, 4.0, 0.0,

            // bottom
            -1.0,   1.0, -1.0,    0.0, 0.0, 5.0, 0.0, 
             1.0,   1.0, -1.0,    0.0, 1.0, 5.0, 0.0,
             1.0,  -1.0, -1.0,    1.0, 1.0, 5.0, 0.0,
            -1.0,  -1.0, -1.0,    1.0, 0.0, 5.0, 0.0,

            // top
            -1.0,  -1.0,  1.0,    0.0, 0.0, 6.0, 0.0, 
             1.0,  -1.0,  1.0,    0.0, 1.0, 6.0, 0.0,
             1.0,   1.0,  1.0,    1.0, 1.0, 6.0, 0.0,
            -1.0,   1.0,  1.0,    1.0, 0.0, 6.0, 0.0
        ];

        let cube_indices = [
             0,  1,  2,  0,  2,  3, // front
             4,  5,  6,  4,  6,  7, // back
             8,  9, 10,  8, 10, 11, // left
            12, 13, 14, 12, 14, 15, // right
            16, 17, 18, 16, 18, 19, // bottom
            20, 21, 22, 20, 22, 23  // top
        ];

        this.bufCube = new VertexBuffer(
            [ { data : cube_attrib, attrSize: 3, attrStride: 7*4, attrLoc: this.progDraw.inPos },
              { attrSize: 4, attrStride: 7*4, attrOffset: 3*4, attrLoc: this.progDraw.inAttr } ],
              cube_indices );

        this.resize(); 
        this.camera = new Camera( [0, 3, 0.0], [0, 0, 0], [0, 0, 1], 90, this.vp_size, 0.5, 100 );
        this.track = new TrackControl( this.vp_size );
        this.track.attenuation( 1.0, 0.05, 0.0 );
        
        this.rubiks = new Rubiks(2.0 * 1.1, 1.0 / 3.0);
        this.shuffles = 11;
        this.rubiks.shuffle( this.shuffles );
        this.rubiks.animationTime( 1.0 );

        this.start_hit = this.init_hit();
        this.hit_pt = [0, 0, 0];

        var self = this;
        window.onresize = function() { self.resize(); }
    }    

    resize() {
        //vp_size = [gl.drawingBufferWidth, gl.drawingBufferHeight];
        this.vp_size = [window.innerWidth, window.innerHeight]
        this.canvas.width = this.vp_size[0];
        this.canvas.height = this.vp_size[1];
    }

    updateHelpText() {
        let on_shuffle = this.rubiks.pending_queue && this.rubiks.pending_queue.length > 0 && this.rubiks.pending_queue[0].shuffle;
        let on_rotate  = this.track.auto_rotate_mode;
        let act_state = on_shuffle ? this.rubiks.pending_queue.length + 10 : on_rotate ? 1 : 0
        
        if ( this.last_state && act_state == this.last_state )
            return;
            this.last_state = act_state;

        if (on_shuffle) {
            document.getElementById( "line_0" ).innerHTML = "shuffle " + this.rubiks.pending_queue.length + " / " + this.shuffles;
            document.getElementById( "line_1" ).innerHTML = ""; 
        } else if (this.track.auto_rotate_mode) {
            document.getElementById( "line_0" ).innerHTML = "left drag:   rotate cube";
            document.getElementById( "line_1" ).innerHTML = "middle or right click: switch to change mode"; 
        } else {
            document.getElementById( "line_0" ).innerHTML = "left drag:   change";
            document.getElementById( "line_1" ).innerHTML = "middle or right click: switch to rotate mode"; 
        }
    }

    draw(deltaMS){
        
        this.updateRenderData(deltaMS);

        gl.viewport( 0, 0, this.vp_size[0], this.vp_size[1] );
        gl.enable( gl.DEPTH_TEST );
        gl.clearColor( 0.0, 0.0, 0.0, 1.0 );
        gl.clear( gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT );

        // set up draw shader
        this.progDraw.Use();
        this.progDraw.SetM44("u_projection", this.projection);
        this.progDraw.SetM44("u_view", this.view);
        this.progDraw.SetM44("u_model", this.model);
        
        this.progDraw.SetI1("u_cube_hit", this.rubiks.cube_hit);
        this.progDraw.SetI1("u_side_hit", this.rubiks.side_hit);
        for ( let z=0; z<3; ++ z ) {
            for ( let y=0; y<3; ++ y ) {
                for ( let x=0; x<3; ++ x ) {
                    let i = z * 9 + y * 3 + x;
                    this.progDraw.SetI1("u_cubeInstanceID", i);
                    this.progDraw.SetM44("u_rubiks_model", this.rubiks.getCubeModel(x, y, z) );
                    this.bufCube.Draw();           
                }
            }
        }
    }

    init_hit() { return { side: -1, sub_cube: -1, mapped_cube: -1, cube_side: -1 }; }

    updateRenderData(deltaMS) {

        this.updateHelpText();

        // this.rubiks.update( Data.now() );
        this.rubiks.update( deltaMS );
        this.camera.update( this.vp_size );
        this.track.update( this.vp_size );

        this.projection = this.camera.perspective;
        this.view       = this.camera.lookAt;
        this.model      = Mat44.multiply(this.track.orbitMatrix, this.track.autoModelMatrix);

        let modelview          = Mat44.multiply(this.view, this.model);
        let inverse_modelview  = Mat44.inverse(modelview);
        let inverse_projection = Mat44.inverse(this.projection);

        this.rubiks.resetHit();

        if ( this.rubiks.animationPending || this.track.auto_rotate_mode || !this.track.hit )
            this.start_hit = this.init_hit();

        if ( this.rubiks.animationPending || this.track.auto_rotate_mode ) {
            this.track.hit = false;
            this.rubiks.resetHit();
            return;
        }

        let cube_offset = this.rubiks.offset;
        let cube_scale  = this.rubiks.scale;

        // calculate the NDC position of the cursor on the far plane and the camera position
        let w = this.vp_size[0];
        let h = this.vp_size[1];
        let x = this.track.mouse_x;
        let y = this.track.mouse_y;
        let ndc_x = 2.0 * x/w - 1.0;
        let ndc_y = 1.0 - 2.0 * y/h;

        let new_hit = this.init_hit();
        if ( Math.abs( ndc_x ) > 1.0 || Math.abs( ndc_y ) > 1.0 )
            return;

        // calculate a ray from the eye position along the line of sight through the cursor position
        let ndc_cursor_far = [ ndc_x, ndc_y, 1.0 ]; // z = 1.0 -> far plane
        let view_cursor = Mat44.transformPt3(inverse_projection, ndc_cursor_far);

        let view_r0 = [0.0, 0.0, 0.0];
        let view_r1 = view_cursor;

        let model_r0 = Mat44.transformPt3(inverse_modelview, view_r0);
        let model_r1 = Mat44.transformPt3(inverse_modelview, view_r1); 

        let r0_ray = model_r0;
        let d_ray  = Vec3.normalize(Vec3.sub(model_r1, r0_ray));

        if ( this.track.hit && this.start_hit.mapped_cube >= 0 ) {

            this.rubiks.cube_hit = this.start_hit.mapped_cube;
            this.rubiks.side_hit = this.start_hit.cube_side + 1;

            // get 2nd point on intersection plane
            let isect_sub_cube = this.rubiks.intersectSidePlane(r0_ray, d_ray, this.start_hit.side);
            if ( !isect_sub_cube )
                return;

            // check if the length of the vector, which is defined by the 2 intersection points, exceeds the threshold  
            let hover_dir = Vec3.sub(isect_sub_cube.pt, this.hit_pt);
            let threshold_dist = 2.0 * cube_scale * 0.75;
            if ( Vec3.len(hover_dir) < threshold_dist )
                return;
            let s = [Math.abs(hover_dir[0]), Math.abs(hover_dir[1]), Math.abs(hover_dir[2])];

            // get rotation direction vector
            let max_i = s[0] > s[1] ? (s[0] > s[2] ? 0 : 2) : (s[1] > s[2] ? 1 : 2);
            let rot_dir = [0.0, 0.0, 0.0];
            rot_dir[max_i] = hover_dir[max_i] < 0.0 ? -1.0 : 1.0;
            
            // get side direction
            let side_dir = [0.0, 0.0, 0.0];
            side_dir[Math.floor(this.start_hit.side / 2)] = this.start_hit.side % 2 > 0 ? 1.0 : -1.0;

            // get rotation axis, row and direction
            let rot_axis = Vec3.cross(rot_dir, side_dir);
            let axis = this.rubiks.axis(rot_axis);
            let op = {
                axis: axis,
                direction: this.rubiks.direction(rot_axis),
                row: this.rubiks.row(axis, this.start_hit.sub_cube),
            };

            // change the cube
            this.hit = false;
            this.start_hit = this.init_hit();
            this.rubiks.change( op );
            return;
        }

         // find the nearest intersection of a side of the cube and the ray
        let isect = this.rubiks.intersect(r0_ray, d_ray);
        new_hit.side = isect == undefined ? -1 : isect.side_i;
       
        if ( isect ) {
            // get intersected sub cube
            let isect_sub_cube = this.rubiks.intersectedSubCube( new_hit.side, isect.pt );
            new_hit.sub_cube = isect_sub_cube  == undefined ? -1 : isect_sub_cube.cube_i; 
            new_hit.mapped_cube = isect_sub_cube  == undefined ? -1 : isect_sub_cube.mapped_cube_i;
        
            // get the side on the intersected sub cube
            let isect_sub_cube_side = this.rubiks.intersectedSubCubeSide( new_hit.side, new_hit.mapped_cube );
            new_hit.cube_side = isect_sub_cube_side == undefined ? -1 : isect_sub_cube_side;
        }
        
        // set the hit data
        this.rubiks.cube_hit = new_hit.mapped_cube;
        this.rubiks.side_hit = new_hit.cube_side + 1;

        if ( isect && this.track.hit )
        {
            this.start_hit = new_hit;
            this.hit_pt    = isect.pt;
        }
    }
}


class Rubiks {

    constructor( offset, scale ) {
        
        this.c_rot_mat = [
            [ 1,  0,  0, 0,     0, 0, -1, 0,    0,  1, 0, 0,    0, 0, 0, 1 ],
            [ 1,  0,  0, 0,     0, 0,  1, 0,    0, -1, 0, 0,    0, 0, 0, 1 ],
            [ 0,  0,  1, 0,     0, 1,  0, 0,   -1,  0, 0, 0,    0, 0, 0, 1 ],
            [ 0,  0, -1, 0,     0, 1,  0, 0,    1,  0, 0, 0,    0, 0, 0, 1 ],
            [ 0, -1,  0, 0,     1, 0,  0, 0,    0,  0, 1, 0,    0, 0, 0, 1 ],
            [ 0,  1,  0, 0,    -1, 0,  0, 0,    0,  0, 1, 0,    0, 0, 0, 1 ]
        ];

        this.c_no_of_cubes = 27;
        this.animation_time_s = 1.0;

        this.resetHit();
        this.initGeometry( offset, scale );

        this.current_pos = [];
        this.animation = []
        this.model = [];
        for ( let i=0; i<this.c_no_of_cubes; ++i ) {
            this.current_pos.push( Mat44.ident() );
            this.animation.push( Mat44.ident() );
            this.model.push( Mat44.ident() );
        }

        this.updateM44Cubes();
    }

    get animationActive()  { return this.animation_is_active; }
    get animationPending()  { return this.animation_is_active || (this.pending_queue && this.pending_queue.length > 0); }
 
    animationTime( time ) { this.animation_time_s; }

    getCubeModel( x, y, z ) { return this.model[z * 9 + y * 3 + x]; }

    static axisVector( index ) {
        let axis_vec = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
        return axis_vec[index];
    }    

    axis( rot_axis ) {
        return Math.abs(rot_axis[0]) > 0.5 ? 0 : (Math.abs(rot_axis[1]) > 0.5 ?  1 : 2);
    }

    direction( rot_axis ) {
        return (rot_axis[0] + rot_axis[1] + rot_axis[2]) > 0.0 ? 0 : 1;
    }

    row( axis, sub_cube_i )
    {
        switch (axis) {
            case 0: return sub_cube_i % 3;
            case 1: return Math.floor((sub_cube_i % 9) / 3);
            case 2: return Math.floor(sub_cube_i / 9);
        }
        return 1;
    }

    initGeometry( offset, scale ) {
        this.offset = offset;
        this.scale  = scale;
        
        this.cube_map = []
        this.trans_scale = [];
        for ( let z=0; z<3; ++ z ) {
            for ( let y=0; y<3; ++ y ) {
                for ( let x=0; x<3; ++ x ) {
                    let i = z * 9 + y * 3 + x;
                    this.cube_map.push( i );
                    let m = Mat44.scale( Mat44.ident(), [this.scale, this.scale, this.scale] );
                    m = Mat44.translate( m, [this.offset * (x-1), this.offset * (y-1), this.offset * (z-1)] )
                    this.trans_scale.push( m );
                }
            }
        }
    }

    shuffle( steps ) {

        let shuffle_ops = []
        for ( let i = 0; i < steps; ++ i ) {
            let op = {}
            let valid = false;
            do {
                op = {
                    axis: Math.floor((Math.random() * 3) % 3),
                    row: Math.floor((Math.random() * 3) % 3),
                    direction: Math.floor((Math.random() * 2) % 2),
                    shuffle: true
                };

                if ( i == 0 )
                    break;

                // check if not inverse operation
                valid = shuffle_ops[i-1].axis      != op.axis ||
                        shuffle_ops[i-1].row       != op.row ||
                        shuffle_ops[i-1].direction == op.direction;

                // check if not 3 equal operations in a row
                if ( valid && i > 1 ) {
                    valid = shuffle_ops[i-1].axis      != op.axis ||
                            shuffle_ops[i-1].row       != op.row ||
                            shuffle_ops[i-1].direction != op.direction ||
                            shuffle_ops[i-2].axis      != op.axis ||
                            shuffle_ops[i-2].row       != op.row ||
                            shuffle_ops[i-2].direction != op.direction; 
                }    

            } while (!valid);

            shuffle_ops.push( op );
        }

        for ( let i=0; i<shuffle_ops.length; ++i ) 
            this.change( shuffle_ops[i] );    
    }


    change( op ) {
        this.pending_queue = this.pending_queue || [];
        this.pending_queue.push( op );
    }

    update( time_ms ) {
        let pending_changes = this.pending_queue && this.pending_queue.length > 0;
        this.animation_is_active = this.animation_is_active && pending_changes;
        this.current_time = time_ms;
        
        if ( !pending_changes )
            return;
        let op = this.pending_queue[this.pending_queue.length-1];
        if ( this.animation_is_active ) {
            
            let past_animation_time_s = (this.current_time - this.animation_start_time) / 1000.0;
            if ( past_animation_time_s < this.animation_time_s )
            {
                // get change information
                let cube_indices = this.subCubeIndices( op.axis, op.row );

                // update the position model matrix of the affected sub cubes
                for ( let i = 0; i < cube_indices.length; ++ i )
                {
                    let cube_i = cube_indices[i];
                    let angle  = ( 90.0 * Math.PI / 180.0 ) * (op.direction == 0 ? -1.0 : 1.0);
                    angle *= past_animation_time_s / this.animation_time_s;
                    this.animation[cube_i] = Mat44.rotate( Mat44.ident(), angle, Rubiks.axisVector(op.axis) );
                }

                // Update the final model matrices of the sub cubes
                this.updateM44Cubes();
                
                return;
            }
            this.animation_is_active = false;
            
        } else if ( pending_changes ) {
            this.animation_is_active = true;
            this.animation_start_time = time_ms;
            return;
        }

        this.pending_queue.splice(this.pending_queue.length-1, 1);
        this.rotate( op );

        // Update the final model matrices of the sub cubes
        this.updateM44Cubes();
    }

    resetHit() {
        this.cube_hit = -1;
        this.cube_side = 0;
    }

    intersectSidePlane(r0_ray, d_ray, side_i) {
    
        // define the cube corner points and its faces
        let cube_pts = [ 
            [-1.0, -1.0, -1.0], // 0 : left  front bottom
            [ 1.0, -1.0, -1.0], // 1 : right front bottom
            [-1.0,  1.0, -1.0], // 2 : left  back  bottom
            [ 1.0,  1.0, -1.0], // 3 : right back  bottom
            [-1.0, -1.0,  1.0], // 4 : left  front top
            [ 1.0, -1.0,  1.0], // 5 : right front top
            [-1.0,  1.0,  1.0], // 6 : left  back  top
            [ 1.0,  1.0,  1.0]  // 7 : right back  top
        ];
        let cube_faces = [
            [2, 0, 4, 6], // 0 : left
            [1, 3, 7, 5], // 1 : right
            [0, 1, 5, 4], // 2 : front
            [2, 3, 7, 6], // 3 : back
            [0, 1, 3, 2], // 4 : bottom
            [4, 5, 7, 6]  // 5 : top
        ];

        let cube_sidelen_2 = (this.offset + 1.0) * this.scale; // half side length of the entire cube

        // calculate the normal vector of the cube side
        let pa = cube_pts[cube_faces[side_i][0]];
        let pb = cube_pts[cube_faces[side_i][1]];
        let pc = cube_pts[cube_faces[side_i][3]];

        let dir      = Vec3.normalize( d_ray );
        let n_plane  = Vec3.normalize( Vec3.cross( Vec3.sub(pb, pa), Vec3.sub(pc, pa) ) );
        let p0_plane = Vec3.mul_s( pa, cube_sidelen_2 );

        if ( Math.abs( Math.abs( Vec3.dot( dir, n_plane ) ) - 1.0 ) < 0.0017 ) // 0.0017 < sin(1°)
            return;

        let dist  = Vec3.dot(Vec3.sub(pa, r0_ray), n_plane) / Vec3.dot(dir, n_plane);
        let isect = { dist: dist, pt: Vec3.add(r0_ray, Vec3.mul_s(dir, dist)) };
        return isect;
    }

    intersect(r0_ray, d_ray) {
        
        // find the nearest intersection of a side of the cube and the ray 
        let min_isect;
        for ( let i = 0; i < 6; ++ i )
        {
            // calculate the intersection of the ray and a side of the cube map
            let isect = this.intersectSidePlane( r0_ray, d_ray, i )
            if ( !isect )
                continue;

            // check if the intersection point is closer than the previous one
            if ( min_isect && Math.abs(isect.dist) > min_isect.dist )
                continue;

            // check if the intersection is on the side of the cube
            let cube_sidelen_2 = (this.offset + 1.0) * this.scale; // half side length of the entire cube
            let on_side = Math.abs( isect.pt[0] ) < cube_sidelen_2 + 0.001 &&
                          Math.abs( isect.pt[1] ) < cube_sidelen_2 + 0.001 &&
                          Math.abs( isect.pt[2] ) < cube_sidelen_2 + 0.001;
            if ( !on_side )
                continue;  

            min_isect = { side_i: i, dist: Math.abs(isect.dist), pt: isect.pt };
        }
        return min_isect;
    }

    intersectedSubCube(side_i, pt) {
        
        if ( side_i < 0 )
            return;

        // get intersected sub cube
        let i_coord = [];
        let hit_is_on = true;
        for ( let j = 0; j < pt.length; ++ j )
        {
            let i = -1;
            if ( Math.abs( pt[j] ) <= 1.0 * this.scale )
                i = 1;
            else if ( pt[j] <= -(this.offset - 1.0) * this.scale)
                i = 0;
            else if ( pt[j] >= (this.offset - 1.0) * this.scale)
                i = 2;
            i_coord.push( i );
            hit_is_on = hit_is_on && i >= 0;
        }

        if ( !hit_is_on )
            return;
        
        let i = 9 * i_coord[2] + 3 * i_coord[1] + i_coord[0];
        let hit = { cube_i: i, mapped_cube_i: this.cube_map[i] };
        return hit;
    }

    intersectedSubCubeSide(side_i, cube_i) {
        
        if ( side_i < 0 || cube_i < 0 )
            return;

        // get the side on the intersected sub cube
        let cube_mat4 = this.current_pos[cube_i];
        let axis_i    = Math.floor(side_i / 2);
        let sign      = side_i % 2 > 0 ? 1.0 : -1.0;
        let test_vec  = Vec3.mul_s([cube_mat4[axis_i], cube_mat4[4+axis_i], cube_mat4[8+axis_i]], sign);
        
        let isect_i_side = -1;
        if ( test_vec[0] < -0.5 )
            isect_i_side = 0;
        else if ( test_vec[0] > 0.5 )
            isect_i_side = 1;
        else if ( test_vec[1] < -0.5 )
            isect_i_side = 2;
        else if ( test_vec[1] > 0.5 )
            isect_i_side = 3;
        else if ( test_vec[2] < -0.5 )
            isect_i_side = 4;
        else if ( test_vec[2] > 0.5 )
            isect_i_side = 5;

        if ( isect_i_side < 0 ) 
            return
        return isect_i_side;
    }
 
    updateM44Cubes() {
        for ( let i = 0; i < this.c_no_of_cubes; ++i )
            this.model[i] = Mat44.multiply( this.animation[i], Mat44.multiply( this.current_pos[i], this.trans_scale[i] ) );
    }

    rotate( op ) {
        
        let cube_indices = this.subCubeIndices( op.axis, op.row );

        // update the position model matrix of the affected sub cubes 
        let rot_mat = this.c_rot_mat[op.axis*2 + op.direction];
        for ( let i = 0; i < cube_indices.length; ++ i )
        {
            let cube_i = cube_indices[i];
            this.current_pos[cube_i] = Mat44.multiply(rot_mat, this.current_pos[cube_i]);
        }
  
        // Recalculate the index map of the cubes
        let indices = [[0, 0], [1, 0], [2, 0], [2, 1], [2, 2], [1, 2], [0, 2], [0, 1]];

        let current_map = this.cube_map.slice(0);
        for ( let i = 0; i < 8; ++ i )
        {
            let j = ( op.direction == 0 ? i + 6 : i + 2 ) % 8;
            
            let ao = [0, 0, 0], an = [0, 0, 0];
            ao[op.axis] = op.row;
            an[op.axis] = op.row;
            ao[(op.axis+1) % 3] = indices[i][0];
            an[(op.axis+1) % 3] = indices[j][0];
            ao[(op.axis+2) % 3] = indices[i][1];
            an[(op.axis+2) % 3] = indices[j][1];

            let io  = ao[0] + ao[1] * 3 + ao[2] * 9;
            let inx = an[0] + an[1] * 3 + an[2] * 9;

            this.cube_map[inx] = current_map[io];
        }

        // reset animation matrices
        for ( let i=0; i < this.c_no_of_cubes; ++i )
            this.animation[i] = Mat44.ident();
    }

    subCubeIndices( axis, row ) {
        
        // define the affected sub cubes
        let r_x = [0, 2];
        let r_y = [0, 2];
        let r_z = [0, 2];
        switch ( axis ) {
            case 0: r_x[0] = r_x[1] = row; break;
            case 1: r_y[0] = r_y[1] = row; break;
            case 2: r_z[0] = r_z[1] = row; break;
        }

        // collect indices of collected sub cubes 
        let indices = [];
        for ( let z = r_z[0]; z <= r_z[1]; ++ z ) {
            for ( let y = r_y[0]; y <= r_y[1]; ++ y ) {
                for ( let x = r_x[0]; x <= r_x[1]; ++ x ) {
                    let i = z * 9 + y * 3 + x;
                    indices.push( this.cube_map[i] );
                }
            }
        }
        return indices;
    }
}


class Camera {
    constructor( pos, target, up, fov_y, vp, near, far ) {
        this.pos = pos;
        this.target = target;
        this.up = up;
        this.fov_y = fov_y;
        this.vp = vp;
        this.near = near;
        this.far = far;
    }

    get ortho() {
        let fn = this.far + this.near;
        let f_n = this.far - this.near;
        let w = this.vp[0];
        let h = this.vp[1];
        let a = w/h;
        return [ 2/a, 0, 0, 0, 0, 2, 0, 0, 0, 0, -2/f_n, 0, 0, 0, -fn/f_n, 1 ];
    }  

    get perspective() {
        let n = this.near;
        let f = this.far;
        let fn = f + n;
        let f_n = f - n;
        let r = this.vp[0] / this.vp[1];
        let t = 1 / Math.tan( Math.PI * this.fov_y / 360 );
        return [ t/r, 0, 0, 0, 0, t, 0, 0, 0, 0, -fn/f_n, -1, 0, 0, -2*f*n/f_n, 0 ];
    } 

    get lookAt() {
        let mz = Vec3.normalize( [ this.pos[0]-this.target[0], this.pos[1]-this.target[1], this.pos[2]-this.target[2] ] );
        let mx = Vec3.normalize( Vec3.cross( this.up, mz ) );
        let my = Vec3.normalize( Vec3.cross( mz, mx ) );
        let tx = Vec3.dot( mx, this.pos );
        let ty = Vec3.dot( my, this.pos );
        let tz = Vec3.dot( [-mz[0], -mz[1], -mz[2]], this.pos ); 
        return [mx[0], my[0], mz[0], 0, mx[1], my[1], mz[1], 0, mx[2], my[2], mz[2], 0, tx, ty, tz, 1]; 
    }

    update(vp_size, fov_y) {
        if (vp_size)
            this.vp = vp_size;
        if (fov_y)
            this.fov_y = fov_y;
    }
}

class TrackControl {
    constructor( vp ) {
        this.vp = vp;
        this.orbit_mat = this.current_orbit_mat = this.model_mat = this.current_model_mat = Mat44.ident();
        this.mouse_drag = this.auto_spin = false;
        this.auto_rotate = true;
        this.mouse_start = [0, 0];
        this.mouse_drag_axis = [0, 0, 0];
        this.mouse_drag_angle = 0;
        this.mouse_drag_time = 0;
        this.drag_start_T = this.rotate_start_T = this.time;
        this.auto_rotate_mode = true;
        this.hit = false;
        this.mouse_x = 0;
        this.mouse_y = 0;

        this.domElement = document;
        var track = this;
        this.domElement.addEventListener( 'mousedown', function(e) { track.onMouseDown(e) }, false );
        this.domElement.addEventListener( 'mouseup', function(e) { track.onMouseUp(e) }, false );
        this.domElement.addEventListener( 'mousemove', function(e) { track.onMouseMove(e) }, false );
        //this.domElement.addEventListener( 'contextmenu', function(e) { event.preventDefault(); }, false );
        //this.domElement.addEventListener( 'mousewheel', hid_events.onMouseWheel, false );
        //this.domElement.addEventListener( 'DOMMouseScroll', hid_events.onMouseWheel, false ); // firefox
    }

    get time() { return Date.now(); }
        
    get orbit() {
      return this.orbitMatrix;
    }

    get orbitMatrix() {
      return (this.mouse_drag || (this.auto_rotate && this.auto_spin)) ? Mat44.multiply(this.current_orbit_mat, this.orbit_mat) : this.orbit_mat;
    }

    get autoModelMatrix() {
      return this.auto_rotate ? Mat44.multiply(this.current_model_mat, this.model_mat) : this.model_mat;
    }

    attenuation( att_const, att_linear, att_quad ) { this.attenuation = [att_const, att_linear || 0, att_quad || 0]; }

    update(vp_size) {
        if (vp_size)
            this.vp = vp_size;
        let current_T = this.time;
        this.current_model_mat = Mat44.ident()
        if (this.mouse_drag) {
            this.current_orbit_mat = Mat44.rotate(Mat44.ident(), this.mouse_drag_angle, this.mouse_drag_axis);
        } else if (this.auto_rotate) {
            if (this.auto_spin) {
                if (this.mouse_drag_time > 0 ) {
                    let angle = this.mouse_drag_angle * (current_T - this.rotate_start_T) / this.mouse_drag_time;
                    if ( this.attenuation && Math.abs(this.attenuation[0] || 0) > 0.001 )
                        angle /= this.attenuation[0] + this.attenuation[1] * angle + this.attenuation[2] * angle*angle;
                    this.current_orbit_mat = Mat44.rotate(Mat44.ident(), angle, this.mouse_drag_axis);
                }
            } else {
                let auto_angle_x = Util.fract( (current_T - this.rotate_start_T) / 13000.0 ) * 2.0 * Math.PI;
                let auto_angle_y = Util.fract( (current_T - this.rotate_start_T) / 17000.0 ) * 2.0 * Math.PI;
                this.current_model_mat = Mat44.rotateAxis( this.current_model_mat, auto_angle_x, 0 );
                this.current_model_mat = Mat44.rotateAxis( this.current_model_mat, auto_angle_y, 1 );
            }
        }
    }

    changeMotionMode(drag, spin, auto ) {
        let new_drag = drag;
        let new_auto = new_drag ? false : auto;
        let new_spin = new_auto ? spin : false;
        let change = this.mouse_drag != new_drag || this.auto_rotate != new_auto || this.auto_spin != new_spin; 
        if (!change)
            return;
        if (new_drag && !this.mouse_drag) {
            this.drag_start_T = this.time;
            this.mouse_drag_angle = 0.0;
            this.mouse_drag_time = 0;
        }
        if (new_auto && !this.auto_rotate)
            this.rotate_start_T = this.time;
        this.mouse_drag = new_drag; 
        this.auto_rotate = new_auto;  
        this.auto_spin = new_spin;
        this.orbit_mat = Mat44.multiply(this.current_orbit_mat, this.orbit_mat);
        this.current_orbit_mat = Mat44.ident();
        this.model_mat = Mat44.multiply(this.current_model_mat, this.model_mat);
        this.current_model_mat = Mat44.ident();
    }

    onMouseDown( event ) {
        this.mouse_x = event.clientX;
        this.mouse_y = event.clientY;
        let rect = gl.canvas.getBoundingClientRect();
        if ( event.clientX < rect.left || event.clientX > rect.right ) return;
        if ( event.clientY < rect.top || event.clientY > rect.bottom ) return;
        this.hit = false;
        if (event.button == 0) { // left button
            if ( this.auto_rotate_mode ) {
                this.mouse_start = [event.clientX, event.clientY]; 
                this.changeMotionMode( true, false, false );
            } else {
                this.hit = true;
            }
        }
    }

    onMouseUp( event ) {
        this.mouse_x = event.clientX;
        this.mouse_y = event.clientY;
        this.hit = false;
        if (event.button == 0) { // left button
            if ( this.auto_rotate_mode )
                this.changeMotionMode( false, true, true );
        } else if (event.button == 1 || event.button == 2) {// middle or right button
            this.changeMotionMode( false, false, !this.auto_rotate );
            this.auto_rotate_mode = this.auto_rotate;
        }
    }

    onMouseMove( event ) {
        this.mouse_x = event.clientX;
        this.mouse_y = event.clientY;
        let dx = (event.clientX-this.mouse_start[0]) / this.vp[0];
        let dy = (event.clientY-this.mouse_start[1]) / this.vp[1];
        let len = Math.sqrt(dx*dx + dy*dy);
        if (this.mouse_drag && len > 0) {
            this.mouse_drag_angle = Math.PI*len;
            this.mouse_drag_axis = [-dy/len, 0, dx/len];
            this.mouse_drag_time = this.time - this.drag_start_T;
        }
    }
}

let Util = {
    fract : function( val ) { 
        return val - Math.trunc( val );
    },
    angle : function( deltaTime, intervall ) {
        return this.fract( deltaTime / (1000*intervall) ) * 2.0 * Math.PI;
    },
    move : function( deltaTime, intervall, range ) {
        var pos = this.fract( deltaTime / (1000*intervall) ) * 2.0
        var pos = pos < 1.0 ? pos : (2.0-pos)
        return range[0] + (range[1] - range[0]) * pos;
    },    
    ellipticalPosition : function( a, b, angRag ) {
        var a_b = a * a - b * b
        var ea = (a_b <= 0) ? 0 : Math.sqrt( a_b );
        var eb = (a_b >= 0) ? 0 : Math.sqrt( -a_b );
        return [ a * Math.sin( angRag ) - ea, b * Math.cos( angRag ) - eb, 0 ];
    }
}

Vec3 = {
    mul_s: function( v, s ) { return [v[0]*s, v[1]*s, v[2]*s]; },
    add: function( a, b ) { return [a[0]+b[0], a[1]+b[1], a[2]+b[2]]; },
    sub: function( a, b ) { return [a[0]-b[0], a[1]-b[1], a[2]-b[2]]; },
    cross: function( a, b ) { return [ a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0] ]; },
    dot: function( a, b ) { return a[0]*b[0] + a[1]*b[1] + a[2]*b[2]; },
    len: function( v ) { return Math.sqrt( v[0] * v[0] + v[1] * v[1] + v[2] * v[2] ); },
    normalize: function( v ) {
        var len = this.len( v );
        return [ v[0] / len, v[1] / len, v[2] / len ];
    }
}

Mat44 = {
    ident: function() { return [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]; },
    translate: function( matA, trans ) {
        var matB = matA.slice(0);
        for ( var i = 0; i < 3; ++ i )
            matB[12+i] = matA[i] * trans[0] + matA[4+i] * trans[1] + matA[8+i] * trans[2] + matA[12+i];
        return matB;
    },
    scale: function( matA, scale ) {
        var matB = matA.slice(0);
        for ( var a = 0; a < 4; ++ a )
            for ( var i = 0; i < 3; ++ i )
                matB[a*4+i] = matA[a*4+i] * scale[i];
        return matB;
    },
    rotateAxis: function(matA, angRad, axis) {
        var aMap = [ [1, 2], [2, 0], [0, 1] ];
        var a0 = aMap[axis][0], a1 = aMap[axis][1]; 
        var sinAng = Math.sin(angRad), cosAng = Math.cos(angRad);
        var matB = matA.slice(0);
        for ( var i = 0; i < 3; ++ i ) {
            matB[a0*4+i] = matA[a0*4+i] * cosAng + matA[a1*4+i] * sinAng;
            matB[a1*4+i] = matA[a0*4+i] * -sinAng + matA[a1*4+i] * cosAng;
        }
        return matB;
    },
    rotate: function(matA, angRad, axis) {
        var s = Math.sin(-angRad), c = Math.cos(-angRad);
        var x = axis[0], y = axis[1], z = axis[2]; 
        matB = [
            x*x*(1-c)+c,   x*y*(1-c)-z*s, x*z*(1-c)+y*s, 0,
            y*x*(1-c)+z*s, y*y*(1-c)+c,   y*z*(1-c)-x*s, 0,
            z*x*(1-c)-y*s, z*y*(1-c)+x*s, z*z*(1-c)+c,   0,
            0,             0,             0,             1 ];
        return this.multiply(matA, matB);
    },
    multiply: function(matA, matB) {
        matC = this.ident();
        for (var i0=0; i0<4; ++i0 )
            for (var i1=0; i1<4; ++i1 )
                matC[i0*4+i1] = matB[i0*4+0] * matA[0*4+i1] + matB[i0*4+1] * matA[1*4+i1] + matB[i0*4+2] * matA[2*4+i1] + matB[i0*4+3] * matA[3*4+i1]  
        return matC;
    },

    inverse: function( m ) {
        
        let vec4_add = function( a, b ) { return [ a[0]+b[0], a[1]+b[1], a[2]+b[2], a[3]+b[3] ]; }
        let vec4_sub = function( a, b ) { return [ a[0]-b[0], a[1]-b[1], a[2]-b[2], a[3]-b[3] ]; }
        let vec4_mul = function( a, b ) { return [ a[0]*b[0], a[1]*b[1], a[2]*b[2], a[3]*b[3] ]; }
        let vec4_scale = function( a, s ) { return [ a[0]*s, a[1]*s, a[2]*s, a[3]*s ]; }

        let Coef00 = m[2*4+2] * m[3*4+3] - m[3*4+2] * m[2*4+3];
        let Coef02 = m[1*4+2] * m[3*4+3] - m[3*4+2] * m[1*4+3];
        let Coef03 = m[1*4+2] * m[2*4+3] - m[2*4+2] * m[1*4+3];    
        let Coef04 = m[2*4+1] * m[3*4+3] - m[3*4+1] * m[2*4+3];
        let Coef06 = m[1*4+1] * m[3*4+3] - m[3*4+1] * m[1*4+3];
        let Coef07 = m[1*4+1] * m[2*4+3] - m[2*4+1] * m[1*4+3];   
        let Coef08 = m[2*4+1] * m[3*4+2] - m[3*4+1] * m[2*4+2];
        let Coef10 = m[1*4+1] * m[3*4+2] - m[3*4+1] * m[1*4+2];
        let Coef11 = m[1*4+1] * m[2*4+2] - m[2*4+1] * m[1*4+2];   
        let Coef12 = m[2*4+0] * m[3*4+3] - m[3*4+0] * m[2*4+3];
        let Coef14 = m[1*4+0] * m[3*4+3] - m[3*4+0] * m[1*4+3];
        let Coef15 = m[1*4+0] * m[2*4+3] - m[2*4+0] * m[1*4+3];   
        let Coef16 = m[2*4+0] * m[3*4+2] - m[3*4+0] * m[2*4+2];
        let Coef18 = m[1*4+0] * m[3*4+2] - m[3*4+0] * m[1*4+2];
        let Coef19 = m[1*4+0] * m[2*4+2] - m[2*4+0] * m[1*4+2];   
        let Coef20 = m[2*4+0] * m[3*4+1] - m[3*4+0] * m[2*4+1];
        let Coef22 = m[1*4+0] * m[3*4+1] - m[3*4+0] * m[1*4+1];
        let Coef23 = m[1*4+0] * m[2*4+1] - m[2*4+0] * m[1*4+1];
        
        let Fac0 = [Coef00, Coef00, Coef02, Coef03];
        let Fac1 = [Coef04, Coef04, Coef06, Coef07];
        let Fac2 = [Coef08, Coef08, Coef10, Coef11];
        let Fac3 = [Coef12, Coef12, Coef14, Coef15];
        let Fac4 = [Coef16, Coef16, Coef18, Coef19];
        let Fac5 = [Coef20, Coef20, Coef22, Coef23];
        
        let Vec0 = [ m[1*4+0], m[0*4+0], m[0*4+0], m[0*4+0] ];
        let Vec1 = [ m[1*4+1], m[0*4+1], m[0*4+1], m[0*4+1] ];
        let Vec2 = [ m[1*4+2], m[0*4+2], m[0*4+2], m[0*4+2] ];
        let Vec3 = [ m[1*4+3], m[0*4+3], m[0*4+3], m[0*4+3] ];
        
        let Inv0 = vec4_add( vec4_sub( vec4_mul(Vec1, Fac0), vec4_mul(Vec2, Fac1) ), vec4_mul( Vec3, Fac2 ) );
        let Inv1 = vec4_add( vec4_sub( vec4_mul(Vec0, Fac0), vec4_mul(Vec2, Fac3) ), vec4_mul( Vec3, Fac4 ) );
        let Inv2 = vec4_add( vec4_sub( vec4_mul(Vec0, Fac1), vec4_mul(Vec1, Fac3) ), vec4_mul( Vec3, Fac5 ) );
        let Inv3 = vec4_add( vec4_sub( vec4_mul(Vec0, Fac2), vec4_mul(Vec1, Fac4) ), vec4_mul( Vec2, Fac5 ) );
        
        let SignA = [+1.0, -1.0, +1.0, -1.0];
        let SignB = [-1.0, +1.0, -1.0, +1.0];
        let Inverse = [ vec4_mul(Inv0, SignA), vec4_mul(Inv1, SignB), vec4_mul(Inv2, SignA), vec4_mul(Inv3, SignB) ];
        
        let Row0 = [Inverse[0][0], Inverse[1][0], Inverse[2][0], Inverse[3][0] ];
        
        let Dot0 = [Row0[0], Row0[1], Row0[2], Row0[3] ];
        Dot0 = vec4_mul( Dot0, [ m[0], m[1], m[2], m[3] ] );
        let Dot1 = (Dot0[0] + Dot0[1]) + (Dot0[2] + Dot0[3]);
        
        let OneOverDeterminant = 1 / Dot1;

        var res = this.ident();  
        for ( var inx1 = 0; inx1 < 4; inx1 ++ ) {
            for ( var inx2 = 0; inx2 < 4; inx2 ++ )
                res[inx1*4+inx2] = Inverse[inx1][inx2] * OneOverDeterminant;
        }
        return res;
    },

    transformVec3: function(mat, vec) {
        return [
            vec[0] * mat[0*4+0] + vec[1] * mat[1*4+0] + vec[2] * mat[2*4+0],
            vec[0] * mat[0*4+1] + vec[1] * mat[1*4+1] + vec[2] * mat[2*4+1],
            vec[0] * mat[0*4+2] + vec[1] * mat[1*4+2] + vec[2] * mat[2*4+2] ]
    },

    transformPt3: function(mat, vec) {
        var h = [
            vec[0] * mat[0*4+0] + vec[1] * mat[1*4+0] + vec[2] * mat[2*4+0] + mat[3*4+0],
            vec[0] * mat[0*4+1] + vec[1] * mat[1*4+1] + vec[2] * mat[2*4+1] + mat[3*4+1],
            vec[0] * mat[0*4+2] + vec[1] * mat[1*4+2] + vec[2] * mat[2*4+2] + mat[3*4+2],
            vec[0] * mat[0*4+3] + vec[1] * mat[1*4+3] + vec[2] * mat[2*4+3] + mat[3*4+3] ]
        if ( h[3] == 0.0 )
            return [0, 0, 0]
        return [ h[0]/h[3], h[1]/h[3], h[2]/h[3] ];
    }
}

class VertexBuffer {
    constructor(attribs, indices, type) {
        this.buf = [];
        this.attr = [];
        this.inx = gl.createBuffer();
        this.inxLen = indices.length;
        this.primitive_type = type ? type : gl.TRIANGLES;
        for (let i=0; i<attribs.length; ++i) {
            let stride = attribs[i].attrStride || 0;
            let offset = attribs[i].attrOffset || 0;
            let no_of  = attribs[i].data ? attribs[i].data.length/attribs[i].attrSize : 0;
            this.attr.push({ size : attribs[i].attrSize, loc : attribs[i].attrLoc, no_of: no_of, stride: stride, offset: offset });
            if ( attribs[i].data ) {
                this.buf.push(gl.createBuffer());
                gl.bindBuffer(gl.ARRAY_BUFFER, this.buf[i]);
                gl.bufferData(gl.ARRAY_BUFFER, new Float32Array( attribs[i].data ), gl.STATIC_DRAW);
            } else {
                this.buf.push( null );
            }
        }
        gl.bindBuffer(gl.ARRAY_BUFFER, null);
        if ( this.inxLen > 0 ) {
            gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.inx);
            gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array( indices ), gl.STATIC_DRAW);
            gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);
        }
    }

    Draw() {
        for (let i=0; i<this.buf.length; ++i) {
            if ( this.buf[i] != null )
                gl.bindBuffer(gl.ARRAY_BUFFER, this.buf[i]);
            gl.vertexAttribPointer(this.attr[i].loc, this.attr[i].size, gl.FLOAT, false, this.attr[i].stride, this.attr[i].offset);
            gl.enableVertexAttribArray(this.attr[i].loc);
        }
        if (this.inxLen > 0) {
            gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.inx);
            gl.drawElements(this.primitive_type, this.inxLen, gl.UNSIGNED_SHORT, 0);
            gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);
        }
        else
            gl.drawArrays(this.primitive_type, 0, this.attr[0].no_of);
        for (let i=0; i<this.buf.length; ++i)
            gl.disableVertexAttribArray(this.attr[i].loc);
        gl.bindBuffer(gl.ARRAY_BUFFER, null);
    }
}

class ShProg {
    constructor(shaderList) {
        let shaderObjs = [];
        for (let i_sh = 0; i_sh < shaderList.length; ++i_sh) {
            let shderObj = this.Compile(shaderList[i_sh].source, shaderList[i_sh].stage);
            if (shderObj) shaderObjs.push(shderObj);
        }
        this.progObj = this.Link(shaderObjs)
        if (this.progObj) {
            this.attrInx = {};
            var noOfAttributes = gl.getProgramParameter(this.progObj, gl.ACTIVE_ATTRIBUTES);
            for (var i_n = 0; i_n < noOfAttributes; ++i_n) {
                var name = gl.getActiveAttrib(this.progObj, i_n).name;
                this.attrInx[name] = gl.getAttribLocation(this.progObj, name);
            }
            this.uniLoc = {};
            var noOfUniforms = gl.getProgramParameter(this.progObj, gl.ACTIVE_UNIFORMS);
            for (var i_n = 0; i_n < noOfUniforms; ++i_n) {
                var name = gl.getActiveUniform(this.progObj, i_n).name;
                this.uniLoc[name] = gl.getUniformLocation(this.progObj, name);
            }
        }
    }

    AttrI(name) { return this.attrInx[name]; }
    UniformL(name) { return this.uniLoc[name]; }
    Use() { gl.useProgram(this.progObj); }
    SetI1(name, val) { if (this.uniLoc[name]) gl.uniform1i(this.uniLoc[name], val); }
    SetF1(name, val) { if (this.uniLoc[name]) gl.uniform1f(this.uniLoc[name], val); }
    SetF2(name, arr) { if (this.uniLoc[name]) gl.uniform2fv(this.uniLoc[name], arr); }
    SetF3(name, arr) { if (this.uniLoc[name]) gl.uniform3fv(this.uniLoc[name], arr); }
    SetF4(name, arr) { if (this.uniLoc[name]) gl.uniform4fv(this.uniLoc[name], arr); }
    SetM33(name, mat) { if (this.uniLoc[name]) gl.uniformMatrix3fv(this.uniLoc[name], false, mat); }
    SetM44(name, mat) { if (this.uniLoc[name]) gl.uniformMatrix4fv(this.uniLoc[name], false, mat); }
    
    Compile(source, shaderStage) {
        let shaderScript = document.getElementById(source);
        if (shaderScript)
            source = shaderScript.text;
        let shaderObj = gl.createShader(shaderStage);
        gl.shaderSource(shaderObj, source);
        gl.compileShader(shaderObj);
        let status = gl.getShaderParameter(shaderObj, gl.COMPILE_STATUS);
        if (!status) alert(gl.getShaderInfoLog(shaderObj));
        return status ? shaderObj : null;
    }
    
    Link(shaderObjs) {
        let prog = gl.createProgram();
        for (let i_sh = 0; i_sh < shaderObjs.length; ++i_sh)
            gl.attachShader(prog, shaderObjs[i_sh]);
        gl.linkProgram(prog);
        status = gl.getProgramParameter(prog, gl.LINK_STATUS);
        if ( !status ) alert(gl.getProgramInfoLog(prog));
        return status ? prog : null;
    } 
}


function render(deltaMS){
    app.draw(deltaMS);
    requestAnimationFrame(render);
}

let app = new App();
requestAnimationFrame(render);


})();
</script>

</body>